/**
 * @author Liu Cong
 */
(function(){
const AMFConstants = FireAMF.Constants;
const AMFException = FireAMF.Exception;

FireAMF.AMF0 = {};

/**
 * Read an AMF0 input stream and convert it into JavaScript data types
 *
 * @package    AMF0
 */
FireAMF.AMF0.Deserializer = function(stream){
	this._stream = stream;
	
    /**
     * An array of objects used for recursivly deserializing an object.
     * @var array
     */
	this._reference = [];
	
    /**
     * If AMF3 serialization occurs, update to AMF0 0x03
     *
     * @var int
     */
	this._objectEncoding = AMFConstants.AMF0_OBJECT_ENCODING;
	
    /**
     * refrence to AMF3 deserializer
     *
     * @var FireAMF.AMF3.Deserializer
     */
	this._deserializer = null;
};

FireAMF.AMF0.Deserializer.prototype = {
    /**
     * Read AMF markers and dispatch for deserialization
     *
     * Checks for AMF marker types and calls the appropriate methods
     * for deserializing those marker types. Markers are the data type of
     * the following value.
     *
     * @param  integer typeMarker
     * @return mixed whatever the data type is of the marker in php
     * @return mixed
     * @throws AMFException for invalid type
     */
	readTypeMarker: function(typeMarker){
        if (typeMarker === undefined) {
            typeMarker = this._stream.readByte();
        }

        switch(typeMarker) {
            // number
            case AMFConstants.AMF0_NUMBER:
                return this._stream.readDouble();

            // boolean
            case AMFConstants.AMF0_BOOLEAN:
                return !!this._stream.readByte();

            // string
            case AMFConstants.AMF0_STRING:
                return this._stream.readUTF();

            // object
            case AMFConstants.AMF0_OBJECT:
                return this.readObject();

            // null
            case AMFConstants.AMF0_NULL:
                return null;

            // undefined
            case AMFConstants.AMF0_UNDEFINED:
                return undefined;

            // Circular references are returned here
            case AMFConstants.AMF0_REFERENCE:
                return this.readReference();

            // mixed array with numeric and string keys
            case AMFConstants.AMF0_MIXEDARRAY:
                return this.readMixedArray();

            // array
            case AMFConstants.AMF0_ARRAY:
                return this.readArray();

            // date
            case AMFConstants.AMF0_DATE:
                return this.readDate();

            // longString  strlen(string) > 2^16
            case AMFConstants.AMF0_LONGSTRING:
                return this._stream.readLongUTF();

            //internal AS object,  not supported
            case AMFConstants.AMF0_UNSUPPORTED:
                return null;

            // XML
            case AMFConstants.AMF0_XML:
                return this.readXmlString();

            // typed object ie Custom Class
            case AMFConstants.AMF0_TYPEDOBJECT:
                return this.readTypedObject();

            //AMF3-specific
            case AMFConstants.AMF0_AMF3:
                return this.readAmf3TypeMarker();

            default:
                throw new AMFException('Unsupported marker type: ' + typeMarker);
        }
	},

    /**
     * Read AMF objects and convert to JavaScript objects
     *
     * Read the name value pair objects form the php message and convert them to
     * a javascript object class.
     *
     * Called when the marker type is 3.
     *
     * @param  array|null object
     * @return object
     */
    readObject: function(object){
        if (object === undefined) {
            object = {};
        }

        while (true) {
            var key        = this._stream.readUTF();
            var typeMarker = this._stream.readByte();
            if (typeMarker != AMFConstants.AMF0_OBJECTTERM ){
                //Recursivly call readTypeMarker to get the types of properties in the object
                object[key] = this.readTypeMarker(typeMarker);
            } else {
                //encountered AMF object terminator
                break;
            }
        }
        this._reference.push(object);
        return object;
	},

    /**
     * Read reference objects
     *
     * Used to gain access to the private array of refrence objects.
     * Called when marker type is 7.
     *
     * @return object
     * @throws AMFException for invalid reference keys
     */
    readReference: function(){
        var key = this._stream.readInt();
        if (!this._reference[key]) {
            throw new AMFException('Invalid reference key: ' + key);
        }
        return this._reference[key];
    },
	
    /**
     * Reads an array with numeric and string indexes.
     *
     * Called when marker type is 8
     *
     * @todo   As of Flash Player 9 there is not support for mixed typed arrays
     *         so we handle this as an object. With the introduction of vectors
     *         in Flash Player 10 this may need to be reconsidered.
     * @return array
     */
    readMixedArray: function() {
        var length = this._stream.readLong();
        return this.readObject();
    },
	
    /**
     * Converts numberically indexed actiosncript arrays into JavaScript arrays.
     *
     * Called when marker type is 10
     *
     * @return array
     */
    readArray: function() {
        var length = this._stream.readLong();
        var array = [];
        while (length--) {
            array.push(this.readTypeMarker());
        }
        return array;
    },

    /**
     * Convert AS Date to Zend_Date
     *
     * @return Zend_Date
     */
    readDate: function() {
        // get the unix time stamp. Not sure why ActionScript does not use
        // milliseconds
        var timestamp = Math.floor(this._stream.readDouble() / 1000);

        // The timezone offset is never returned to the server; it is always 0,
        // so read and ignore.
        var offset = this._stream.readInt();

        var date   = new Date(timestamp);
        return date;
    },
	
    /**
     * Convert XML to E4X XML object
     *
     * @return XML Object
     */
    readXmlString: function() {
        var string = this._stream.readLongUTF();
        return new XML(string);
    },
	
    /**
     * Read Class that is to be mapped to a server class.
     *
     * Commonly used for Value Objects on the server
     *
     * @todo   implement Typed Class mapping
     * @return object
     * @throws AMFException if unable to load type
     */
    readTypedObject: function() {
        // get the remote class name
        var className = this._stream.readUTF();
        var loader = FireAMF.TypeLoader.loadType(className);
        var returnObject = new loader();
		var remoteObject = this.readObject();
		for(var key in remoteObject) {
			if(key) returnObject[key] = remoteObject[key];
		}
        return returnObject;
    },
	
    /**
     * AMF3 data type encountered load AMF3 Deserializer to handle
     * type markers.
     *
     * @return string
     */
    readAmf3TypeMarker: function() {
        var deserializer = this.getDeserializer();
        this._objectEncoding = AMFConstants.AMF3_OBJECT_ENCODING;
        return deserializer.readTypeMarker();
    },
	
    /**
     * Return the object encoding to check if an AMF3 object
     * is going to be return.
     *
     * @return int
     */
    getObjectEncoding: function() {
        return this._objectEncoding;
    },

    /**
     * Get deserializer
     *
     * @return Zend_Amf_Parse_Amf3_Deserializer
     */
    getDeserializer: function() {
        if (null === this._deserializer) {
            this._deserializer = new FireAMF.AMF3.Deserializer(this._stream);
        }
        return this._deserializer;
    }
};

FireAMF.AMF3 = {};
/**
 * Read an AMF3 input stream and convert it into JavaScript data types.
 *
 * @package    AMF3
 */
FireAMF.AMF3.Deserializer = function(stream){
	this._stream = stream;

    /**
     * An array of reference objects per amf body
     * @var array
     */
    this._referenceObjects = [];

    /**
     * An array of reference strings per amf body
     * @var array
     */
    this._referenceStrings = [];

    /**
     * An array of reference class definitions per body
     * @var array
     */
    this._referenceDefinitions = [];
};

FireAMF.AMF3.Deserializer.prototype = {
    /**
     * Read AMF markers and dispatch for deserialization
     *
     * Checks for AMF marker types and calls the appropriate methods
     * for deserializing those marker types. markers are the data type of
     * the following value.
     *
     * @param  integer typeMarker
     * @return mixed Whatever the corresponding JavaScript data type is
     * @throws AMFException for unidentified marker type
     */
    readTypeMarker: function(typeMarker) {
        if(undefined === typeMarker) {
            typeMarker = this._stream.readByte();
        }

        switch(typeMarker) {
            case AMFConstants.AMF3_UNDEFINED:
                 return undefined;
            case AMFConstants.AMF3_NULL:
                 return null;
            case AMFConstants.AMF3_BOOLEAN_FALSE:
                 return false;
            case AMFConstants.AMF3_BOOLEAN_TRUE:
                 return true;
            case AMFConstants.AMF3_INTEGER:
                 return this.readInteger();
            case AMFConstants.AMF3_NUMBER:
                 return this._stream.readDouble();
            case AMFConstants.AMF3_STRING:
                 return this.readString();
            case AMFConstants.AMF3_DATE:
                 return this.readDate();
            case AMFConstants.AMF3_ARRAY:
                 return this.readArray();
            case AMFConstants.AMF3_OBJECT:
                 return this.readObject();
            case AMFConstants.AMF3_XML:
            case AMFConstants.AMF3_XMLSTRING:
                 return this.readXmlString();
            case AMFConstants.AMF3_BYTEARRAY:
                 return this.readString();
            default:
                throw new AMFException('Unsupported type marker: ' + typeMarker);
        }
    },
	
    /**
     * Read and deserialize an integer
     *
     * AMF 3 represents smaller integers with fewer bytes using the most
     * significant bit of each byte. The worst case uses 32-bits
     * to represent a 29-bit number, which is what we would have
     * done with no compression.
     * - 0x00000000 - 0x0000007F : 0xxxxxxx
     * - 0x00000080 - 0x00003FFF : 1xxxxxxx 0xxxxxxx
     * - 0x00004000 - 0x001FFFFF : 1xxxxxxx 1xxxxxxx 0xxxxxxx
     * - 0x00200000 - 0x3FFFFFFF : 1xxxxxxx 1xxxxxxx 1xxxxxxx xxxxxxxx
     * - 0x40000000 - 0xFFFFFFFF : throw range exception
     *
     *
     * 0x04 -> integer type code, followed by up to 4 bytes of data.
     *
     * @see:   Parsing integers on OSFlash {http://osflash.org/amf3/parsing_integers>} for the AMF3 integer data format.
     * @return int|float
     */
    readInteger: function() {
        var count        = 1;
        var intReference = this._stream.readByte();
        var result       = 0;
        while (((intReference & 0x80) != 0) && count < 4) {
            result       <<= 7;
            result        |= (intReference & 0x7f);
            intReference   = this._stream.readByte();
            count++;
        }
        if (count < 4) {
            result <<= 7;
            result  |= intReference;
        } else {
            // Use all 8 bits from the 4th byte
            result <<= 8;
            result  |= intReference;

            // Check if the integer should be negative
            if ((result & 0x10000000) != 0) {
                //and extend the sign bit
                result |= 0xe0000000;
            }
        }
        return result;
    },
	
    /**
     * Read and deserialize a string
     *
     * Strings can be sent as a reference to a previously
     * occurring String by using an index to the implicit string reference table.
     * Strings are encoding using UTF-8 - however the header may either
     * describe a string literal or a string reference.
     *
     * - string = 0x06 string-data
     * - string-data = integer-data [ modified-utf-8 ]
     * - modified-utf-8 = *OCTET
     *
     * @return String
     */
    readString: function() {
        var stringReference = this.readInteger();

        //Check if this is a reference string
        if ((stringReference & 0x01) == 0) {
            // reference string
            stringReference = stringReference >> 1;
            if (stringReference >= this._referenceStrings.length) {
                throw new AMFException('Undefined string reference: ' + stringReference);
            }
            // reference string found
            return this._referenceStrings[stringReference];
        }

        var length = stringReference >> 1;
        if (length) {
            var string = this._stream.readBytes(length);
            this._referenceStrings.push(string);
        } else {
            var string = "";
        }
        return string;
    },

    /**
     * Read and deserialize a date
     *
     * Data is the number of milliseconds elapsed since the epoch
     * of midnight, 1st Jan 1970 in the UTC time zone.
     * Local time zone information is not sent to flash.
     *
     * - date = 0x08 integer-data [ number-data ]
     *
     * @return Zend_Date
     */
    readDate: function() {
        var dateReference = this.readInteger();
        if ((dateReference & 0x01) == 0) {
            dateReference = dateReference >> 1;
            if (dateReference>=this._referenceObjects.length) {
                throw new AMFException('Undefined date reference: ' + dateReference);
            }
            return this._referenceObjects[dateReference];
        }

        var timestamp = Math.floor(this._stream.readDouble() / 1000);

        var dateTime  = new Date(timestamp);
        this._referenceObjects.push(dateTime);
        return dateTime;
    },
	
    /**
     * Read amf array to JavaScript array
     *
     * - array = 0x09 integer-data ( [ 1OCTET *amf3-data ] | [OCTET *amf3-data 1] | [ OCTET *amf-data ] )
     *
     * @return array
     */
    readArray: function() {
        var arrayReference = this.readInteger();
        if ((arrayReference & 0x01)==0){
            arrayReference = arrayReference >> 1;
            if (arrayReference>=count(this._referenceObjects)) {
                throw new AMFException('Unknow array reference: ' + arrayReference);
            }
            return this._referenceObjects[arrayReference];
        }

        // Create a holder for the array in the reference list
        var data = {};
        this._referenceObjects.push(data);
        var key = this.readString();

        // Iterating for string based keys.
        while (key != '') {
            data[key] = this.readTypeMarker();
            key = this.readString();
        }

        arrayReference = arrayReference >>1;

        //We have a dense array
        for (i=0; i < arrayReference; i++) {
            data[i] = this.readTypeMarker();
        }

        return data;
    },
	
    /**
     * Read an object from the AMF stream and convert it into a JavaScript object
     *
     * @todo   Rather than using an array of traitsInfo create Zend_Amf_Value_TraitsInfo
     * @return object
     */
    readObject: function() {
        var traitsInfo   = this.readInteger();
        var storedObject = (traitsInfo & 0x01)==0;
        traitsInfo   = traitsInfo >> 1;

        // Check if the Object is in the stored Objects reference table
        if (storedObject) {
            var ref = traitsInfo;
            if (!this._referenceObjects[ref]) {
                throw new AMFException('Unknown Object reference: ' + ref);
            }
            var returnObject = this._referenceObjects[ref];
        } else {
            // Check if the Object is in the stored Definistions reference table
            storedClass = (traitsInfo & 0x01) == 0;
            traitsInfo  = traitsInfo >> 1;
            if (storedClass) {
                var ref = traitsInfo;
                if (!this._referenceDefinitions[ref]) {
                    throw new AMFException('Unknows Definition reference: '+ ref);
                }
                // Populate the reference attributes
                var className     = this._referenceDefinitions[ref]['className'];
                var encoding      = this._referenceDefinitions[ref]['encoding'];
                var propertyNames = this._referenceDefinitions[ref]['propertyNames'];
            } else {
                // The class was not in the reference tables. Start reading rawdata to build traits.
                // Create a traits table. Zend_Amf_Value_TraitsInfo would be ideal
                var className     = this.readString();
                var encoding      = traitsInfo & 0x03;
                var propertyNames = [];
                traitsInfo    = traitsInfo >> 2;
            }

            // We now have the object traits defined in variables. Time to go to work:
            if (!className) {
                // No class name generic object
                var returnObject = new Object();
            } else {
                // Defined object
                // Typed object lookup agsinst registered classname maps
				var loader;
                if (loader = FireAMF.TypeLoader.loadType(className)) {
                    var returnObject = new loader();
                } else {
                    //user defined typed object
                    throw new AMFException('Typed object not found: '+ className + ' ');
                }
            }

            // Add the Object ot the reference table
            this._referenceObjects.push(returnObject);

            // Check encoding types for additional processing.
            switch (encoding) {
                case (AMFConstants.ET_EXTERNAL):
                    // Externalizable object such as {ArrayCollection} and {ObjectProxy}
                    if (!storedClass) {
                        this._referenceDefinitions.push({
                            'className'     : className,
                            'encoding'      : encoding,
                            'propertyNames' : propertyNames,
                        });
                    }
                    returnObject.externalizedData = this.readTypeMarker();
                    break;
                case (AMFConstants.ET_DYNAMIC):
                    // used for Name-value encoding
                    if (!storedClass) {
                        this._referenceDefinitions.push({
                            'className'     : className,
                            'encoding'      : encoding,
                            'propertyNames' : propertyNames,
                        });
                    }
                    // not a refrence object read name value properties from byte stream
                    var properties = {}; // clear value
                    do {
                        var property = this.readString();
                        if (property != "") {
                            propertyNames.push(property);
                            properties[property] = this.readTypeMarker();
                        }
                    } while (property !="");
                    break;
                default:
                    // basic property list object.
                    if (!storedClass) {
                        var count = traitsInfo; // Number of properties in the list
                        for(i=0; i< count; i++) {
                            propertyNames.push(this.readString());
                        }
                        // Add a refrence to the class.
                        this._referenceDefinitions.push({
                            'className'     : className,
                            'encoding'      : encoding,
                            'propertyNames' : propertyNames,
                        });
                    }
                    var properties = {}; // clear value
                    for each (var property in propertyNames) {
                        properties[property] = this.readTypeMarker();
                    }
                    break;
            }

            // Add properties back to the return object.
            for(var key in properties) {
                if(key) {
                    returnObject[key] = properties[key];
                }
            }
        }
        return returnObject;
    },
	
    /**
     * Convert XML to E4X XML Object
     *
     * @return XML Object
     */
    readXmlString: function() {
        var xmlReference = this.readInteger();
        var length = xmlReference >> 1;
        var string = this._stream.readBytes(length);
        return new XML(string);
    }

};

})();
